# include "io.hpp"

# include <unistd.h>
# include <strings.h>

# include <cstdio>

# include <algorithm>
# include <iterator>


namespace {

  /*! @brief File wrapper
   *
   * The main idea behind this class is to be care with exception handling
   */
  class FileWrapper {
  public:

    enum class FileType {
      READ,
      WRITE
    };

  public:
  
    explicit FileWrapper(FILE* stream) throw ()
        : istream_(stream), ioType_(FileType::READ), isDone_(true) {}

    FileWrapper(FILE* stream, FileType tp) throw ()
        : istream_(stream), ioType_(tp), isDone_(false) {}

    FileWrapper(FileWrapper&& fw) throw() 
        : istream_(fw.istream_), ioType_(fw.ioType_), isDone_(fw.isDone_) {
      fw.istream_ = nullptr;
    }

    ~FileWrapper() {
      if (istream_ != nullptr) {
        if (ioType_ == FileType::WRITE && isDone_)
          fflush(istream_);
        fclose(istream_);
      }
    }

    void done() throw() { isDone_ = true; }

    inline FILE* stream() throw() { return istream_; }

  private:

    FileWrapper(FileWrapper const& other) = delete;
    FileWrapper& operator=(FileWrapper const&) = delete;

  private:

    /*! @brief IO stream
     */
    FILE* istream_;

    /*! @brief Type of the stream
     */
    FileType const ioType_;

    /*! brief Result of workning operation
     */
    bool isDone_;
  };
  

  /*! @brief Open file for reading 
   *
   * @param[in] filename name of the file
   * @return opened file stream
   */
  FileWrapper openFile(std::string const& filename) {
    if (access(filename.c_str(), R_OK) == -1)
      throw io::IOException("cannot open "+filename+" for reading");

    FILE* istream = fopen(filename.c_str(), "r");
    if (istream == nullptr)
      throw io::IOException("cannot open file for reading");

    return FileWrapper(istream);
  }


  /*! @brief Open file for writing
   *
   * @param[in] filename name of the file
   * @return wrapper for stream
   */
  FileWrapper openWFile(std::string const& filename) {
    FILE* istream = fopen(filename.c_str(), "w");
    if (istream == nullptr)
      throw io::IOException("cannot open file for writing");

    return FileWrapper(istream, FileWrapper::FileType::WRITE);
  }

  /*! @brief Size of the buffer
   */
  size_t const BUFFER_SIZE = 256;


  /*! @brief Data formatter
   */
  template <typename T>
  struct Format;

  template<>
  struct Format<double> {
    static char const format[];
  };

  template <>
  struct Format<int> {
    static char const format[];
  };

  char const Format<double>::format[] = "%lf %lf";
  char const Format<int>::format[] = "%i %i";


  /*! @brief Read numbers from the buffer
   */
  template <unsigned int NumNumbers, typename T>
  struct Reader;


  /*! @brief Reading two numbers from the buffer
   * 
   * Template specification for two numbers
   */
  template <typename T>
  struct Reader<2, T> {

    /*! @brief Class constructor
     *
     * @param[in,out] output container for parsed elements
     */  
    explicit Reader(std::vector<T>& output) throw() : data(output) {}

    /*! @brief Read portion of data
     */
    inline void read(char* buffer) {
      T x = 0, y = 0;
      int nmemb = sscanf(buffer, Format<T>::format, &x, &y);
      if (nmemb != 2)
        throw io::IOException("insufficient data in container; expected 2");

      data.push_back(x);
      data.push_back(y);
    }

    /*! @brief Output container
     */
    std::vector<T>& data;
  };


  /*! @brier Read numbers into the vector
   *
   * Template specification for three numbers
   */
  template <typename T>
  struct Reader<3, T> {

    /*! @brief Class constructor
     *
     * @param[in,out] output container for parsed elements
     */  
    explicit Reader(std::vector<T>& output) throw() : data(output) {}

    /*! @brief Read portion of data
     */  
    void read(char* buffer) {
      T x = 0, y = 0, z = 0;
      int nmemb = sscanf(buffer, "%lf %lf %lf", &x, &y, &z);
      if (nmemb != 3)
        throw io::IOException("insufficient data container; expected 3");

      data.push_back(x);
      data.push_back(y);
      data.push_back(z);
    }

    /*! @brief Output container
     */
    std::vector<T>& data;

  };

  /*! @brief Shortcuts for data readers
   */
  typedef Reader<2, double> TextureReader;
  typedef Reader<3, double> VertexReader;
  typedef Reader<2, int>    EdgeReader;

}

/*! @brief Load obj-file into the mesh
 *
 * @param[in] filename name of ot the obj-file
 * @return mesh with data
 *
 */
//geometry::Mesh io::loadMeshObj(std::string const& filename) {

//  FileWrapper istream = openFile(filename);
//  char buffer[BUFFER_SIZE];

//  std::vector<double> points;
//  std::vector<double> textures;
//  std::vector<int> edges;

//  VertexReader vr = VertexReader(points);
//  TextureReader tr = TextureReader(textures);
//  EdgeReader er = EdgeReader(edges);

//  while (!feof(istream.stream()) && fgets(buffer, BUFFER_SIZE, istream.stream())) {

//    if (buffer[0] == '#' || buffer[0] == '\n')
//      continue;

//    if (buffer[0] == 'v') {
//      if (buffer[1] == ' ')
//        vr.read(buffer+2);
//      else if (buffer[1] == 't')
//        tr.read(buffer+3);
//    } else if (buffer[0] == 'f')
//      er.read(buffer+2);
//  }

//  return geometry::Mesh(std::move(points), std::move(edges));
//}


/*! @brief Load mesh from Octave format
 *
 * @param[in] filename name of the file with data
 * @return mesh
 */
//geometry::Mesh io::loadMeshOct(std::string const& filename) {

//  FileWrapper istream = openFile(filename);
//  char buffer[BUFFER_SIZE];

//  std::vector<double> points;
//  VertexReader vr = VertexReader(points);
//  while (! feof(istream.stream()) && fgets(buffer, BUFFER_SIZE, istream.stream())) {
//    if (buffer[0] != '#')
//      vr.read(buffer);
//  }

//  return geometry::Mesh(std::move(points));
//}


/*! @brief Load points from the file
 *
 * @param[in] filename name of the file with data
 * @return container with points
 */
std::vector<geometry::d3::Point> io::loadPoints(std::string const& filename) {
  typedef geometry::d3::Point Point;
 
  FileWrapper istream = openFile(filename);

  std::vector<Point> points;
  char buffer[BUFFER_SIZE];
  while (! feof(istream.stream()) && fgets(buffer, BUFFER_SIZE, istream.stream())) {
    if (buffer[0] == '#')
      continue;

    double x = 0, y=  0, z = 0;
    int nnumber = sscanf(buffer, "%lf %lf %lf", &x, &y, &z);
    if (nnumber != 3)
      throw IOException("insufficient number of numbers ");
  
    points.push_back(Point(x, y, z));
  }

  return std::move(points);
}


/*! @brief Write point to file
 *
 * @param[in] filename file for storing points.
 * @param[in] v container (vector) with points
 */
void io::writePoints(std::string const& filename, std::vector<geometry::d3::Point> const& points) {
  typedef geometry::d3::Point Point;

  FileWrapper stream = openWFile(filename);
  for (Point const& p : points) {
    fprintf(stream.stream(), "%f %f %f\n", p.x(), p.y(), p.z());
  }
  stream.done();
}



